<?php

namespace Itwmw\Engine\Route\Ide\Helper;

use Barryvdh\Reflection\DocBlock;
use Barryvdh\Reflection\DocBlock\Context;
use Barryvdh\Reflection\DocBlock\Serializer as DocBlockSerializer;
use Barryvdh\Reflection\DocBlock\Tag;
use Illuminate\Support\Str;
use JetBrains\PhpStorm\Pure;
use phpDocumentor\Reflection\Types\ContextFactory;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionObject;
use ReflectionType;
use W7\Core\Route\RouteDispatcher;
use W7\Core\Route\RouteMapping;

class RouteUriGenerate
{
    protected array $properties = [];

    protected ?string $tableComment;

    public function __construct(protected string $class)
    {
    }

    public function generateCommon()
    {
        if (is_subclass_of($this->class, 'W7\\Core\\Controller\\ControllerAbstract')) {
            $this->makeUriCommonForController();
        } elseif (is_subclass_of($this->class, 'W7\\Core\\Database\\ModelAbstract')) {
            $model = new $this->class();
            $this->getPropertiesFromMethods($model);
            $this->getPropertiesFromTable($model);
            $this->makeCommonForModel($this->class);
        }
    }

    protected function makeCommonForModel($class)
    {
        $reflection  = new ReflectionClass($class);
        $namespace   = $reflection->getNamespaceName();
        $classname   = $reflection->getShortName();
        $originalDoc = $reflection->getDocComment();

        $phpdoc = new DocBlock($reflection, new Context($namespace));

        $phpdoc->setText($this->tableComment ?: ($phpdoc->getText() ?: $class));

        $properties = [];
        foreach ($phpdoc->getTags() as $tag) {
            $name = $tag->getName();
            if ('property' == $name || 'property-read' == $name || 'property-write' == $name) {
                $properties[$tag->getVariableName()] = $tag;
            }
        }

        $propertiesName = array_keys($properties);
        foreach ($this->properties as $name => $property) {
            $name = "\$$name";

            if (in_array($name, $propertiesName)) {
                $phpdoc->deleteTag($properties[$name]);
            }

            if ($property['read'] && $property['write']) {
                $attr = 'property';
            } elseif ($property['write']) {
                $attr = 'property-write';
            } else {
                $attr = 'property-read';
            }

            $tagLine = trim("@{$attr} {$property['type']} {$name} {$property['comment']}");
            $tag     = Tag::createInstance($tagLine, $phpdoc);
            $phpdoc->appendTag($tag);
        }

        $redundantProperties = array_diff($propertiesName, array_map(fn ($key) => "\$$key", array_keys($this->properties)));
        foreach ($redundantProperties as $name) {
            $phpdoc->deleteTag($properties[$name]);
        }

        $serializer      = new DocBlockSerializer(separateTags: true);
        $modelDocComment = $serializer->getDocComment($phpdoc);

        $filename = $reflection->getFileName();
        $contents = file_get_contents($filename);
        if ($originalDoc) {
            $contents = str_replace($originalDoc, $modelDocComment, $contents);
        } else {
            $replace = "{$modelDocComment}\n";
            $pos     = strpos($contents, "final class {$classname}") ?: strpos($contents, "class {$classname}");
            if (false !== $pos) {
                $contents = substr_replace($contents, $replace, $pos, 0);
            }
        }

        file_put_contents($filename, $contents);
    }

    public function getPropertiesFromTable($model)
    {
        $table            = $model->getConnection()->getTablePrefix() . $model->getTable();
        $schema           = $model->getConnection()->getDoctrineSchemaManager();
        $databasePlatform = $schema->getDatabasePlatform();
        $databasePlatform->registerDoctrineTypeMapping('enum', 'string');

        $platformName = $databasePlatform->getName();

        $database = null;
        if (strpos($table, '.')) {
            [$database, $table] = explode('.', $table);
        }

        $this->tableComment = $schema->listTableDetails($table)->getComment();
        $columns            = $schema->listTableColumns($table, $database);

        if (!$columns) {
            return;
        }

        foreach ($columns as $column) {
            $name = $column->getName();
            $type = match ($column->getType()->getName()) {
                'string', 'text', 'date', 'time', 'guid', 'datetimetz', 'datetime', 'decimal' => 'string',
                'integer', 'bigint', 'smallint' => 'integer',
                'boolean' => match ($platformName) {
                    'sqlite', 'mysql' => 'integer',
                    default => 'boolean',
                },
                'float' => 'float',
                'json'  => 'array',
                default => 'mixed',
            };

            $comment = $column->getComment();
            $this->setProperty(
                $name,
                $this->getTypeInModel($model, $type),
                true,
                true,
                $comment,
                !$column->getNotnull()
            );
        }
    }

    protected function getPropertiesFromMethods(object $model)
    {
        $methods = get_class_methods($this->class);
        if ($methods) {
            sort($methods);
            foreach ($methods as $method) {
                if (
                    Str::startsWith($method, 'get') && Str::endsWith(
                        $method,
                        'Attribute'
                    ) && 'getAttribute' !== $method
                ) {
                    $name = Str::snake(substr($method, 3, -9));
                    if (!empty($name)) {
                        $reflection = new ReflectionMethod($model, $method);
                        $type       = $this->getReturnType($reflection);
                        $type       = $this->getTypeInModel($model, $type);
                        $comment    = $this->getCommentFromDocBlock($reflection);
                        $this->setProperty($name, $type, true, null, $comment);
                    }
                } elseif (
                    Str::startsWith($method, 'set') && Str::endsWith(
                        $method,
                        'Attribute'
                    ) && 'setAttribute' !== $method
                ) {
                    $name = Str::snake(substr($method, 3, -9));
                    if (!empty($name)) {
                        $reflection = new ReflectionMethod($model, $method);
                        $comment    = $this->getCommentFromDocBlock($reflection);
                        $this->setProperty($name, null, null, true, $comment);
                    }
                }
            }
        }
    }

    protected function getCommentFromDocBlock(\ReflectionMethod $reflection): ?string
    {
        $phpDocContext = (new ContextFactory())->createFromReflector($reflection);
        $context       = new Context(
            $phpDocContext->getNamespace(),
            $phpDocContext->getNamespaceAliases()
        );
        $comment = '';
        $phpdoc  = new DocBlock($reflection, $context);

        if ($phpdoc->hasTag('comment')) {
            $comment = $phpdoc->getTagsByName('comment')[0]->getContent();
        }

        return $comment;
    }

    protected function getTypeInModel(object $model, ?string $type): ?string
    {
        if (null === $type) {
            return null;
        }

        if (class_exists($type)) {
            $type = $this->getClassNameInDestinationFile($model, $type);
        }

        return $type;
    }

    protected function getClassNameInDestinationFile(object $model, string $className): string
    {
        $reflection = $model instanceof ReflectionClass
            ? $model
            : new ReflectionObject($model)
        ;

        $className = trim($className, '\\');

        if ($reflection->getName() !== $className) {
            return '\\' . $className;
        }

        $usedClassNames = $this->getUsedClassNames($reflection);
        return $usedClassNames[$className] ?? ('\\' . $className);
    }

    protected function getUsedClassNames(ReflectionClass $reflection): array
    {
        $namespaceAliases                         = array_flip((new ContextFactory())->createFromReflector($reflection)->getNamespaceAliases());
        $namespaceAliases[$reflection->getName()] = $reflection->getShortName();

        return $namespaceAliases;
    }

    protected function getReturnType(ReflectionMethod $reflection): ?string
    {
        $type = $this->getReturnTypeFromDocBlock($reflection);
        if ($type) {
            return $type;
        }

        return $this->getReturnTypeFromReflection($reflection);
    }

    protected function getReturnTypeFromDocBlock(ReflectionMethod $reflection)
    {
        $phpDocContext = (new ContextFactory())->createFromReflector($reflection);
        $context       = new Context(
            $phpDocContext->getNamespace(),
            $phpDocContext->getNamespaceAliases()
        );
        $type   = null;
        $phpdoc = new DocBlock($reflection, $context);

        if ($phpdoc->hasTag('return')) {
            $type = $phpdoc->getTagsByName('return')[0]->getType();
        }

        return $type;
    }

    protected function getReturnTypeFromReflection(ReflectionMethod $reflection): ?string
    {
        $returnType = $reflection->getReturnType();
        if (!$returnType) {
            return null;
        }

        $types = $this->extractReflectionTypes($returnType);

        $type = implode('|', $types);

        if ($returnType->allowsNull()) {
            $type .= '|null';
        }

        return $type;
    }

    protected function extractReflectionTypes(ReflectionType $reflection_type): array
    {
        if ($reflection_type instanceof ReflectionNamedType) {
            $types[] = $this->getReflectionNamedType($reflection_type);
        } else {
            $types = [];
            foreach ($reflection_type->getTypes() as $named_type) {
                if ('null' === $named_type->getName()) {
                    continue;
                }

                $types[] = $this->getReflectionNamedType($named_type);
            }
        }

        return $types;
    }

    #[Pure]
    protected function getReflectionNamedType(ReflectionNamedType $paramType): string
    {
        $parameterName = $paramType->getName();
        if (!$paramType->isBuiltin()) {
            $parameterName = '\\' . $parameterName;
        }

        return $parameterName;
    }

    public function setProperty(string $name, string|null $type = null, bool|null $read = null, bool|null $write = null, string|null $comment = '', bool $nullable = false)
    {
        if (!isset($this->properties[$name])) {
            $this->properties[$name]            = [];
            $this->properties[$name]['type']    = 'mixed';
            $this->properties[$name]['read']    = false;
            $this->properties[$name]['write']   = false;
            $this->properties[$name]['comment'] = (string) $comment;
        }
        if (null !== $type) {
            $this->properties[$name]['type'] = $type;
        }
        if (null !== $read) {
            $this->properties[$name]['read'] = $read;
        }
        if (null !== $write) {
            $this->properties[$name]['write'] = $write;
        }
    }

    private function makeUriCommonForController()
    {
        $reflection = new ReflectionClass($this->class);
        if (!$reflection->isInstantiable()) {
            return;
        }

        $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);

        $ignore = [
            '__get', '__set', '__isset', '__unset', '__call', '__autoload', '__construct', '__destruct',
            '__clone', '__toString ', '__sleep', '__wakeup', '__set_state', '__invoke', '__callStatic'
        ];

        $filename = $reflection->getFileName();
        $file     = new File($filename);
        $php      = file_get_contents($filename);

        foreach ($methods as $method) {
            if (in_array($method->getName(), $ignore)) {
                continue;
            }
            if ($method->getDeclaringClass()->getName() != $reflection->getName()) {
                continue;
            }
            if ($method->getFileName() !== $filename) {
                continue;
            }

            try {
                $methodDefine = $file->readLine($method->getStartLine());
                $publicPrefix = $this->getPublicPrefix($methodDefine);
                $uri          = $this->getUrl($reflection->getName(), $method->getName());
                if (!empty($uri)) {
                    $newCommon = $method->getDocComment();
                    $newCommon = $this->makeDocComment($newCommon, $uri, '@uri', $publicPrefix);
                    if ($documentUrl = YApi::instance()->getDocumentUrl($uri)) {
                        $newCommon = $this->makeDocComment($newCommon, $documentUrl, '@document', $publicPrefix);
                    }

                    if (false !== $newCommon) {
                        $oldCommon = $method->getDocComment();
                        if ($oldCommon) {
                            $oldCommonLine = count(explode("\n", $oldCommon));
                            $seq           = $this->getBetweenTexts($oldCommon, $methodDefine, $file->readContent($method->getStartLine() - $oldCommonLine - 1, $method->getStartLine()));
                            $php           = str_replace($oldCommon . $seq . $methodDefine, $newCommon . $seq . $methodDefine, $php);
                        } else {
                            $php = str_replace($methodDefine, $publicPrefix . $newCommon . PHP_EOL . $methodDefine, $php);
                        }
                    }
                }
            } catch (\Exception $e) {
                echo $e;
            }
        }

        file_put_contents($filename, $php);
    }

    private function getBetweenTexts($start, $end, $str): string
    {
        $startPos = mb_strpos($str, $start) + mb_strlen($start);
        $endPos   = mb_strpos($str, $end) - $startPos;
        return mb_substr($str, $startPos, $endPos);
    }

    private function makeDocComment(string $doc, ?string $newComment, string $param, string $publicPrefix)
    {
        $docs = explode("\n", $doc);

        $validatePos = -1;
        for ($i = 0; $i < count($docs); $i++) {
            if (str_contains($docs[$i], $param)) {
                $validatePos = $i;
                break;
            }
        }

        if (null === $newComment) {
            if (-1 === $validatePos) {
                return false;
            }
            unset($docs[$validatePos]);
            return implode("\n", $docs);
        }
        $newValidateDocComment = "$param $newComment";

        if (-1 !== $validatePos) {
            $commonPrefixPos    = strrpos($docs[$validatePos], $param);
            $commonPrefix       = substr($docs[$validatePos], 0, $commonPrefixPos);
            $docs[$validatePos] = $commonPrefix . $newValidateDocComment;
        } else {
            if (count($docs) > 2) {
                $commonPrefixPos = strrpos($docs[1], '*');
                $commonPrefix    = substr($docs[1], 0, $commonPrefixPos);
                array_splice($docs, count($docs) - 1, 0, [$commonPrefix . '* ' . $newValidateDocComment]);
            } else {
                $commonName = '';
                if (preg_match('/\/\*\*([\s\S]*?)\*/', $doc, $match) > 0) {
                    $commonName = $match[1];
                    $commonName = trim(str_replace("\n", '', $commonName));
                }
                $docs   = [];
                $docs[] = '/**';
                if (!empty($commonName)) {
                    $docs[] = $publicPrefix . '* ' . $commonName;
                }
                $docs[] = $publicPrefix . '* ' . $newValidateDocComment;
                $docs[] = $publicPrefix . '*/';
            }
        }

        return implode("\n", $docs);
    }

    private function getPublicPrefix(string $methodDefine)
    {
        $publicPos = strrpos($methodDefine, 'public');
        return substr($methodDefine, 0, $publicPos);
    }

    protected function getUrl(string $controller, string $method)
    {
        $allRoute    = RouteDispatcher::getRouteDefinetions(RouteMapping::class);
        $methodRoute = $allRoute[0];
        foreach ($methodRoute as  $routes) {
            foreach ($routes as $routeInfo) {
                if (!isset($routeInfo['handler'])) {
                    print_r($routeInfo);
                    exit();
                }
                if ($routeInfo['handler'][0] == $controller && $routeInfo['handler'][1] == $method) {
                    return $routeInfo['uri'];
                }
            }
        }

        $regexRoute = $allRoute[1] ?? [];
        foreach ($regexRoute as  $regexRouteInfo) {
            foreach ($regexRouteInfo as $matchingRoutes) {
                foreach ($matchingRoutes['routeMap'] as $routes) {
                    foreach ($routes as $route) {
                        if (isset($route['handler']) && $route['handler'][0] == $controller && $route['handler'][1] == $method) {
                            return $route['uri'];
                        }
                    }
                }
            }
        }

        return null;
    }
}
